package com.itheima.pinda.authority.biz.service.auth.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.itheima.pinda.auth.server.utils.JwtTokenServerUtils;
import com.itheima.pinda.auth.utils.JwtUserInfo;
import com.itheima.pinda.auth.utils.Token;
import com.itheima.pinda.authority.biz.service.auth.ResourceService;
import com.itheima.pinda.authority.biz.service.auth.UserService;
import com.itheima.pinda.authority.dto.auth.LoginDTO;
import com.itheima.pinda.authority.dto.auth.ResourceQueryDTO;
import com.itheima.pinda.authority.dto.auth.UserDTO;
import com.itheima.pinda.authority.entity.auth.Resource;
import com.itheima.pinda.authority.entity.auth.User;
import com.itheima.pinda.base.R;
import com.itheima.pinda.common.constant.CacheKey;
import com.itheima.pinda.dozer.DozerUtils;
import com.itheima.pinda.exception.code.ExceptionCode;
import lombok.extern.slf4j.Slf4j;
import net.oschina.j2cache.CacheChannel;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

/**
 * 认证管理器
 *
 * @author llhuang10
 * @date 2021/8/27 11:37
 */
@Slf4j
@Service
public class AuthManager {


    @Autowired
    private UserService userService;

    @Autowired
    private DozerUtils dozerUtils;

    @Autowired
    private JwtTokenServerUtils jwtTokenServerUtils;

    @Autowired
    private ResourceService resourceService;

    @Autowired
    private CacheChannel cacheChannel;

    /**
     * 登录认证
     * . @NotEmpty(message = "账号不能为空") 没必要加上, controller已经加上校验操作了
     *
     * @param account  账号
     * @param password 密码
     * @return R<LoginDTO>
     */
    public R<LoginDTO> login(String account, String password) {
        // 校验账号, 密码是否正确
        R<User> userR = this.check(account, password);
        Boolean isError = userR.getIsError();

        if (isError) {
            return R.fail("认证失败");
        }

        // 为用户生成jwt令牌
        User user = userR.getData();
        Token token = this.generateUserToken(user);

        // 查询当前用户可以访问的资源权限
        // 这里的实体类Resource 和 注解@Resource 名称一样, 需要注意下
        List<Resource> userResource =
                resourceService.findVisibleResource(ResourceQueryDTO.builder().userId(user.getId()).build());
        log.info("当前用户拥有的资源权限为: {}", userResource);

        List<String> permissionList = null;
        if (userResource != null && userResource.size() > 0) {
            // 将用户对应的权限(表pd_auth_resource字段 code 给前端使用) 传给前端
            permissionList = userResource.stream()
                    .map(Resource::getCode).collect(Collectors.toList());

            // 将用户对应的权限(表pd_auth_resource字段 method + url 给后端网关使用) 进行缓存
            List<String> visibleResource = userResource.stream()
                    .map((resource -> {
                        return resource.getMethod() + resource.getUrl();
                    })).collect(Collectors.toList());

            // 注意点: key的数据类型是 String, 其他类型需要做转换
            cacheChannel.set(CacheKey.USER_RESOURCE, user.getId().toString(), visibleResource);
        }

        // 封装返回结果
        // dozerUtils.map(userR.getData(), UserDTO.class)  将 User转换为UserDTO
        LoginDTO loginDTO = LoginDTO.builder()
                .user(dozerUtils.map(userR.getData(), UserDTO.class))
                .token(token)
                .permissionsList(permissionList)
                .build();

        return R.success(loginDTO);
    }

    /**
     * 账号, 密码校验
     *
     * @return R<User>
     */
    private R<User> check(String account, String password) {
        // 校验账号, 密码是否正确
        User user = userService.getOne(Wrappers.<User>lambdaQuery().eq(User::getAccount, account));
        // 将前端提交的密码进行md5加密
        String md5Hex = DigestUtils.md5Hex(password);
        if (user == null || !user.getPassword().equals(md5Hex)) {
            // todo 认证错误, 返回错误信息, 并将用户的错误密码次数 + 1, 利用redis的 incr命令
            //  错误达到5次的时候, 可以将用户锁住一定时间, 这个根据业务而定 (可选)
            //  密码错误的时候, 前端应该需要触发是一个事件, 刷新验证码 -> 就是重新调用接口生成一次验证码
            return R.fail(ExceptionCode.JWT_USER_INVALID);
        }
        // 认证成功
        // todo 将用户的错误密码次数key重置为0, 或将redis中 错误密码次数key删除
        return R.success(user);
    }

    /**
     * 为当前登录用户生成对应的jwt令牌
     *
     * @param user 当前登录用户
     * @return token
     */
    private Token generateUserToken(User user) {
        JwtUserInfo jwtUserInfo = new JwtUserInfo(user.getId(), user.getAccount(),
                user.getName(), user.getOrgId(), user.getStationId());
        // 这里设计 expire失效时间为null, 这是因为使用的是配置文件里面的 expire
        // 使用的配置文件是 nacos中的 pd-auth-server.yml 配置文件
        return jwtTokenServerUtils.generateUserToken(jwtUserInfo, null);
    }
}
